import tkinter as tk
from datetime import datetime
from tkinter import messagebox, filedialog, simpledialog, ttk
import json
import os
import re
import csv
import hashlib
import pickle
# 使用LangChain的PDF加载器
from langchain.document_loaders import PyMuPDFLoader, UnstructuredPDFLoader
from langchain.llms import Tongyi
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import HuggingFaceEmbeddings
from langchain.vectorstores import FAISS
from langchain.chains import RetrievalQA
from langchain.docstore.document import Document
from PIL import Image, ImageTk
import threading
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
import queue
import multiprocessing


# 创建主窗口
class ExperimentDesignApp:
    def __init__(self, parent):
        self.root = tk.Frame(parent)
        self.root.pack(fill=tk.BOTH, expand=True)

        # 初始化变量
        self.history = ""
        self.conversation_history = []
        self.vectorstore = None
        self.qa_chain = None
        self.pdf_paths = []
        self.pdf_processing_queue = queue.Queue()
        self.processing_errors = []
        self.last_prompt = ""  # 存储最后一次发送给LLM的完整prompt
        self.processed_pdfs = set()  # 跟踪已处理的PDF
        self.cache_dir = "./pdf_cache/"  # PDF处理缓存目录
        os.makedirs(self.cache_dir, exist_ok=True)

        # 初始化通义千文大模型
        self.llm = Tongyi(
            model_name="qwen-7b-chat",
            api_key="sk-159861d025ee4784b77e823b84af4387"
        )

        # 定义自定义的 prompt
        self.custom_prompt = """
        结合以上内容，生成DOE实验设计相应的因子和水平，并以Markdown表格的形式返回，表格格式如下：
        | 因子 | 水平范围 | 单位 |
        | --- | --- | --- |
        其中水平范围中的数值精确到两位数。
        """

        # 创建UI
        self.create_widgets()

    def create_widgets(self):
        # 创建顶部框架 (用于PDF上传)
        top_frame = tk.Frame(self.root)
        top_frame.pack(fill=tk.X, padx=10, pady=5)

        # PDF上传区域
        pdf_frame = tk.LabelFrame(top_frame, text="PDF 文件管理")
        pdf_frame.pack(fill=tk.X, padx=5, pady=5)

        self.pdf_listbox = tk.Listbox(pdf_frame, width=60, height=3)
        self.pdf_listbox.pack(side=tk.LEFT, padx=5, pady=5, fill=tk.X, expand=True)

        upload_btn = tk.Button(pdf_frame, text="上传 PDF", command=self.upload_pdf)
        upload_btn.pack(side=tk.LEFT, padx=5)

        process_btn = tk.Button(pdf_frame, text="处理 PDF", command=self.process_pdfs)
        process_btn.pack(side=tk.LEFT, padx=5)

        clear_btn = tk.Button(pdf_frame, text="清除 PDF", command=self.clear_pdfs)
        clear_btn.pack(side=tk.LEFT, padx=5)

        # PDF处理选项
        pdf_options_frame = tk.Frame(pdf_frame)
        pdf_options_frame.pack(side=tk.RIGHT, padx=5)

        tk.Label(pdf_options_frame, text="加载器:").pack(side=tk.LEFT)
        self.loader_var = tk.StringVar(value="fast")
        tk.OptionMenu(pdf_options_frame, self.loader_var, "fast", "accurate").pack(side=tk.LEFT, padx=5)

        # 创建左右两个框架
        left_frame = tk.Frame(self.root)
        left_frame.pack(side=tk.LEFT, padx=10, pady=10, fill=tk.BOTH, expand=True)

        right_frame = tk.Frame(self.root)
        right_frame.pack(side=tk.RIGHT, padx=10, pady=10, fill=tk.BOTH, expand=True)

        # 左侧：用户输入和结果展示
        input_frame = tk.LabelFrame(left_frame, text="实验设计需求")
        input_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        label = tk.Label(input_frame, text="请输入实验设计需求:")
        label.pack(pady=10, anchor=tk.W)

        self.entry = tk.Entry(input_frame, width=70)
        self.entry.pack(pady=5, fill=tk.X, padx=5)

        # 创建处理按钮
        button_frame = tk.Frame(input_frame)
        button_frame.pack(fill=tk.X, pady=10, padx=5)

        process_btn = tk.Button(button_frame, text="处理需求", command=self.process_input)
        process_btn.pack(side=tk.LEFT, padx=5)

        continue_btn = tk.Button(button_frame, text="继续对话", command=self.continue_conversation)
        continue_btn.pack(side=tk.LEFT, padx=5)

        # 新增：显示RAG上下文按钮
        show_rag_btn = tk.Button(button_frame, text="显示RAG上下文", command=self.show_rag_context)
        show_rag_btn.pack(side=tk.LEFT, padx=5)

        # 创建结果显示文本框
        result_frame = tk.LabelFrame(input_frame, text="LLM 响应结果")
        result_frame.pack(fill=tk.BOTH, expand=True, pady=10, padx=5)

        scrollbar = tk.Scrollbar(result_frame)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.result_text = tk.Text(result_frame, width=60, height=30, yscrollcommand=scrollbar.set)
        self.result_text.pack(pady=5, fill=tk.BOTH, expand=True)
        scrollbar.config(command=self.result_text.yview)

        # 右侧：历史记录和表格显示
        history_frame = tk.LabelFrame(right_frame, text="对话历史")
        history_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        history_scrollbar = tk.Scrollbar(history_frame)
        history_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.history_text = tk.Text(history_frame, width=40, height=20, yscrollcommand=history_scrollbar.set)
        self.history_text.pack(pady=5, fill=tk.BOTH, expand=True)
        history_scrollbar.config(command=self.history_text.yview)

        # 历史操作按钮
        history_button_frame = tk.Frame(history_frame)
        history_button_frame.pack(fill=tk.X, pady=5, padx=5)

        back_btn = tk.Button(history_button_frame, text="返回上一步", command=self.go_back)
        back_btn.pack(side=tk.LEFT, padx=5)

        view_history_btn = tk.Button(history_button_frame, text="查看历史对话", command=self.view_history)
        view_history_btn.pack(side=tk.LEFT, padx=5)

        # 表格显示区域
        table_frame = tk.LabelFrame(right_frame, text="因子和水平表格")
        table_frame.pack(fill=tk.BOTH, expand=True, pady=10, padx=5)

        # 表格操作按钮
        table_buttons_frame = tk.Frame(table_frame)
        table_buttons_frame.pack(fill=tk.X, pady=5, padx=5)

        add_factor_btn = tk.Button(table_buttons_frame, text="添加因子", command=self.add_factor)
        add_factor_btn.pack(side=tk.LEFT, padx=5)

        edit_factor_btn = tk.Button(table_buttons_frame, text="编辑因子", command=self.edit_factor)
        edit_factor_btn.pack(side=tk.LEFT, padx=5)

        delete_factor_btn = tk.Button(table_buttons_frame, text="删除因子", command=self.delete_factor)
        delete_factor_btn.pack(side=tk.LEFT, padx=5)

        export_csv_btn = tk.Button(table_buttons_frame, text="导出CSV", command=self.export_to_csv)
        export_csv_btn.pack(side=tk.LEFT, padx=5)

        show_table_btn = tk.Button(table_buttons_frame, text="显示表格", command=self.show_table_from_result)
        show_table_btn.pack(side=tk.LEFT, padx=5)

        table_scrollbar = tk.Scrollbar(table_frame)
        table_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.table = ttk.Treeview(table_frame, columns=("因子", "水平范围", "单位"), show="headings",
                                  yscrollcommand=table_scrollbar.set)
        self.table.heading("因子", text="因子")
        self.table.heading("水平范围", text="水平范围")
        self.table.heading("单位", text="单位")
        self.table.column("因子", width=150)
        self.table.column("水平范围", width=150)
        self.table.column("单位", width=100)
        self.table.pack(pady=5, fill=tk.BOTH, expand=True)
        table_scrollbar.config(command=self.table.yview)

        # 状态标签
        self.status_var = tk.StringVar()
        self.status_var.set("就绪")
        status_label = tk.Label(self.root, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, anchor=tk.W)
        status_label.pack(side=tk.BOTTOM, fill=tk.X)

    def upload_pdf(self):
        """上传PDF文件"""
        file_paths = filedialog.askopenfilenames(filetypes=[("PDF文件", "*.pdf")])
        if not file_paths:
            return

        for file_path in file_paths:
            if file_path not in self.pdf_paths:
                self.pdf_paths.append(file_path)
                self.pdf_listbox.insert(tk.END, os.path.basename(file_path))

        self.status_var.set(f"已选择 {len(self.pdf_paths)} 个PDF文件")

    def _get_pdf_cache_key(self, pdf_path):
        """生成PDF文件的唯一缓存键"""
        file_stat = os.stat(pdf_path)
        # 使用文件路径、大小和修改时间生成唯一标识
        key_material = f"{pdf_path}-{file_stat.st_size}-{file_stat.st_mtime}".encode()
        return hashlib.md5(key_material).hexdigest()

    def _load_cached_pdf(self, pdf_path):
        """加载缓存的PDF处理结果"""
        cache_key = self._get_pdf_cache_key(pdf_path)
        cache_path = os.path.join(self.cache_dir, f"{cache_key}.pkl")

        if os.path.exists(cache_path):
            try:
                with open(cache_path, 'rb') as f:
                    return pickle.load(f)
            except:
                # 缓存文件损坏，删除它
                os.remove(cache_path)
        return None

    def _save_pdf_cache(self, pdf_path, documents):
        """缓存PDF处理结果"""
        cache_key = self._get_pdf_cache_key(pdf_path)
        cache_path = os.path.join(self.cache_dir, f"{cache_key}.pkl")

        try:
            with open(cache_path, 'wb') as f:
                pickle.dump(documents, f)
            return True
        except Exception as e:
            print(f"缓存PDF失败: {str(e)}")
            return False

    def process_pdfs(self):
        """处理上传的PDF文件并创建向量存储"""
        if not self.pdf_paths:
            messagebox.showerror("错误", "请先上传PDF文件")
            return

        # 禁用处理按钮防止重复点击
        for widget in self.root.winfo_children():
            for child in widget.winfo_children():
                if isinstance(child, tk.Button) and child["text"] == "处理 PDF":
                    child.config(state=tk.DISABLED)

        # 清空处理错误列表
        self.processing_errors = []

        # 显示处理中的提示
        self.status_var.set("正在准备处理PDF文件...")
        self.show_progress_dialog("处理中", "正在准备处理PDF文件...")

        # 在新线程中处理PDF，避免UI卡顿
        threading.Thread(target=self._process_pdfs_thread, daemon=True).start()

    def _process_pdfs_thread(self):
        """在后台线程中处理PDF文件，优化处理速度"""
        try:
            start_time = time.time()
            all_documents = []

            # 获取需要处理的新PDF（排除已处理的）
            new_pdfs = [pdf for pdf in self.pdf_paths if pdf not in self.processed_pdfs]

            if not new_pdfs:
                # 所有PDF都已处理过，直接加载向量存储
                self.root.after(0, self.progress_dialog.destroy)
                self.status_var.set("所有PDF已处理完成")
                messagebox.showinfo("提示", "所有PDF文件已处理完成")
                # 启用处理按钮
                self.root.after(0, lambda: self._enable_process_button())
                return

            # 获取CPU核心数，确定并行线程数（使用更多线程）
            max_workers = min(multiprocessing.cpu_count() * 2, len(new_pdfs))
            if max_workers < 1:
                max_workers = 1

            # 更新进度对话框
            self.root.after(0, lambda: self.progress_dialog.update_status(
                f"将使用 {max_workers} 个线程并行处理 {len(new_pdfs)} 个PDF文件"))

            # 使用线程池并行处理PDF
            with ThreadPoolExecutor(max_workers=max_workers) as executor:
                future_to_pdf = {executor.submit(self._load_single_pdf, pdf_path): pdf_path for pdf_path in new_pdfs}

                for future in as_completed(future_to_pdf):
                    pdf_path = future_to_pdf[future]
                    try:
                        docs = future.result()
                        if docs:
                            all_documents.extend(docs)
                            self.processed_pdfs.add(pdf_path)
                    except Exception as e:
                        self.processing_errors.append(f"处理 {os.path.basename(pdf_path)} 时出错: {str(e)}")

            if not all_documents and not self.vectorstore:
                self.root.after(0, self.progress_dialog.destroy)
                error_msg = "\n".join(self.processing_errors) if self.processing_errors else "未找到有效文档内容"
                self.status_var.set("处理PDF时出错")
                messagebox.showerror("错误", f"处理PDF时出错:\n{error_msg}")
                # 启用处理按钮
                self.root.after(0, lambda: self._enable_process_button())
                return

            # 分割文档 - 优化分割参数
            self.root.after(0, lambda: self.progress_dialog.update_status("正在分割文档..."))
            text_splitter = RecursiveCharacterTextSplitter(
                chunk_size=1500,  # 增大chunk_size减少分割数量
                chunk_overlap=150,  # 适当减少重叠
                separators=["\n\n", "\n", " ", ""]
            )

            # 如果已有向量存储，只分割新文档并合并
            if self.vectorstore:
                split_docs = text_splitter.split_documents(all_documents)
                self.vectorstore.add_documents(split_docs)
            else:
                split_docs = text_splitter.split_documents(all_documents)

                # 创建嵌入模型 - 使用更快的模型
                self.root.after(0, lambda: self.progress_dialog.update_status("正在创建向量存储..."))
                # 使用更小更快的嵌入模型
                embeddings = HuggingFaceEmbeddings(
                    model_name="sentence-transformers/all-MiniLM-L6-v2",
                    model_kwargs={'device': 'cpu'},  # 强制使用CPU避免GPU加载延迟
                    encode_kwargs={'normalize_embeddings': True}
                )

                # 创建向量存储
                self.vectorstore = FAISS.from_documents(split_docs, embeddings)

            # 创建或更新检索链
            self.qa_chain = RetrievalQA.from_chain_type(
                llm=self.llm,
                chain_type="stuff",
                retriever=self.vectorstore.as_retriever(search_kwargs={"k": 3}),
                return_source_documents=True
            )

            # 更新UI
            self.root.after(0, self._on_pdf_processing_complete, start_time, len(new_pdfs))

        except Exception as e:
            self.root.after(0, lambda: self._on_pdf_processing_error(e))
            # 启用处理按钮
            self.root.after(0, lambda: self._enable_process_button())

    def _enable_process_button(self):
        """启用处理按钮"""
        for widget in self.root.winfo_children():
            for child in widget.winfo_children():
                if isinstance(child, tk.Button) and child["text"] == "处理 PDF":
                    child.config(state=tk.NORMAL)

    def _load_single_pdf(self, pdf_path):
        """使用优化的方式加载单个PDF文件"""
        try:
            # 先检查缓存
            cached_docs = self._load_cached_pdf(pdf_path)
            if cached_docs:
                self.pdf_processing_queue.put((pdf_path, "已从缓存加载"))
                return cached_docs

            # 更新进度队列
            self.pdf_processing_queue.put((pdf_path, "开始处理"))

            # 根据选择使用不同的加载器
            if self.loader_var.get() == "fast":
                # 更快但可能精度稍低的加载器
                loader = PyMuPDFLoader(pdf_path)
            else:
                # 更精确但较慢的加载器
                loader = UnstructuredPDFLoader(pdf_path)

            documents = loader.load()

            # 更新进度队列
            page_count = len(documents)
            self.pdf_processing_queue.put((pdf_path, f"已加载 {page_count} 页"))

            # 为每个文档添加来源元数据
            for doc in documents:
                doc.metadata["source"] = os.path.basename(pdf_path)

            # 保存到缓存
            self._save_pdf_cache(pdf_path, documents)

            return documents
        except Exception as e:
            self.pdf_processing_queue.put((pdf_path, f"处理出错: {str(e)}"))
            raise

    def _on_pdf_processing_complete(self, start_time, processed_count):
        """PDF处理完成后的回调"""
        # 关闭进度对话框
        self.progress_dialog.destroy()

        # 启用处理按钮
        self._enable_process_button()

        processing_time = time.time() - start_time

        # 显示处理结果
        success_count = processed_count - len(self.processing_errors)
        error_count = len(self.processing_errors)

        status_msg = f"已处理 {processed_count} 个PDF文件，成功 {success_count} 个，失败 {error_count} 个，耗时: {processing_time:.2f}秒"
        self.status_var.set(status_msg)

        # 构建成功消息
        success_msg = f"PDF文件处理完成！\n已处理 {processed_count} 个PDF文件，成功 {success_count} 个，失败 {error_count} 个\n耗时: {processing_time:.2f}秒\n可以开始基于文档内容进行问答"

        # 添加错误信息（如果有）
        if self.processing_errors:
            success_msg += "\n\n以下文件处理失败:\n" + "\n".join(self.processing_errors)

        messagebox.showinfo("成功", success_msg)

    def _on_pdf_processing_error(self, error):
        """PDF处理错误后的回调"""
        # 关闭进度对话框
        self.progress_dialog.destroy()

        # 启用处理按钮
        self._enable_process_button()

        self.status_var.set(f"处理PDF时出错: {str(error)}")
        messagebox.showerror("错误", f"处理PDF时出错: {str(error)}")

    def show_progress_dialog(self, title, message):
        """显示进度对话框"""
        self.progress_dialog = tk.Toplevel(self.root)
        self.progress_dialog.title(title)
        self.progress_dialog.geometry("500x300")
        self.progress_dialog.transient(self.root)
        self.progress_dialog.grab_set()

        # 居中显示
        self.progress_dialog.update_idletasks()
        width = self.progress_dialog.winfo_width()
        height = self.progress_dialog.winfo_height()
        x = (self.root.winfo_width() // 2) - (width // 2) + self.root.winfo_x()
        y = (self.root.winfo_height() // 2) - (height // 2) + self.root.winfo_y()
        self.progress_dialog.geometry(f"+{x}+{y}")

        # 添加进度信息
        frame = tk.Frame(self.progress_dialog, padx=20, pady=20)
        frame.pack(fill=tk.BOTH, expand=True)

        self.progress_label = tk.Label(frame, text=message, justify=tk.LEFT)
        self.progress_label.pack(pady=10, anchor=tk.W)

        # 添加详细进度文本框
        scrollbar = tk.Scrollbar(frame)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        self.progress_text = tk.Text(frame, width=60, height=10, yscrollcommand=scrollbar.set)
        self.progress_text.pack(pady=10, fill=tk.BOTH, expand=True)
        scrollbar.config(command=self.progress_text.yview)

        # 添加进度条
        self.progress_bar = ttk.Progressbar(frame, mode="indeterminate")
        self.progress_bar.pack(fill=tk.X, pady=10)
        self.progress_bar.start()

        # 开始轮询队列更新进度
        self.root.after(100, self._update_progress_from_queue)

    def _update_progress_from_queue(self):
        """从队列更新进度信息"""
        while not self.pdf_processing_queue.empty():
            pdf_path, status = self.pdf_processing_queue.get()
            self.progress_text.insert(tk.END, f"{os.path.basename(pdf_path)}: {status}\n")
            self.progress_text.see(tk.END)
            self.progress_dialog.update_idletasks()

        # 继续轮询
        if hasattr(self, 'progress_dialog') and self.progress_dialog.winfo_exists():
            self.root.after(100, self._update_progress_from_queue)

    def update_status(self, message):
        """更新进度对话框中的状态信息"""
        self.progress_label.config(text=message)

    def clear_pdfs(self):
        """清除已上传的PDF文件"""
        self.pdf_paths = []
        self.pdf_listbox.delete(0, tk.END)
        self.processed_pdfs = set()
        self.vectorstore = None
        self.qa_chain = None
        self.status_var.set("已清除所有PDF文件")

    def process_input(self):
        """处理用户输入并获取LLM响应"""
        user_input = self.entry.get()
        if not user_input:
            messagebox.showerror("错误", "请输入实验设计需求。")
            return

        # 将用户输入添加到历史对话中
        self.history += user_input + "\n"

        # 在新线程中调用LLM，避免UI卡顿
        self.status_var.set("正在获取LLM响应...")
        threading.Thread(target=self._process_input_thread, args=(user_input,), daemon=True).start()

    def _process_input_thread(self, user_input):
        """在后台线程中处理用户输入，使用LangChain的RAG链"""
        try:
            # 结合历史对话和自定义prompt
            full_prompt = f"{self.history}\n{self.custom_prompt}"

            # 如果有向量存储，使用RAG
            if self.vectorstore and self.qa_chain:
                # 保存完整的prompt（包括RAG上下文）
                self.last_prompt = full_prompt

                # 使用LangChain的QA链进行查询
                result = self.qa_chain({"query": full_prompt})
                response = result["result"]

                # 添加RAG检索的上下文到prompt
                if "source_documents" in result:
                    context = "\n\n===== RAG检索的上下文 ====="
                    for doc in result["source_documents"]:
                        context += f"\n\n来源: {doc.metadata['source']} (第{doc.metadata.get('page', '未知')}页)\n"
                        context += f"{doc.page_content[:500]}..."  # 限制长度为500个字符

                    self.last_prompt += context
            else:
                # 没有向量存储，直接调用LLM
                self.last_prompt = full_prompt
                response = self.llm(full_prompt)

            # 存储这一轮的提问和回答
            self.conversation_history.append((user_input, response))

            # 更新UI
            self.root.after(0, self._update_ui_after_response, response)

        except Exception as e:
            self.root.after(0, lambda: messagebox.showerror("错误", f"调用API时发生错误: {str(e)}"))
            self.status_var.set(f"发生错误: {str(e)}")

    def show_rag_context(self):
        """显示传递给LLM的完整prompt（包括RAG上下文）"""
        if not self.last_prompt:
            messagebox.showinfo("提示", "还没有处理任何需求，请先点击'处理需求'按钮")
            return

        # 创建一个新窗口显示完整prompt
        prompt_window = tk.Toplevel(self.root)
        prompt_window.title("传递给LLM的完整Prompt")
        prompt_window.geometry("800x600")
        prompt_window.transient(self.root)
        prompt_window.grab_set()

        # 添加文本框显示prompt
        frame = tk.Frame(prompt_window, padx=20, pady=20)
        frame.pack(fill=tk.BOTH, expand=True)

        scrollbar = tk.Scrollbar(frame)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)

        prompt_text = tk.Text(frame, width=80, height=30, wrap=tk.WORD, yscrollcommand=scrollbar.set)
        prompt_text.pack(pady=10, fill=tk.BOTH, expand=True)
        scrollbar.config(command=prompt_text.yview)

        # 插入完整prompt
        prompt_text.insert(tk.END, self.last_prompt)

        # 只读模式
        prompt_text.config(state=tk.DISABLED)

        # 添加复制按钮
        button_frame = tk.Frame(prompt_window)
        button_frame.pack(pady=10)

        copy_btn = tk.Button(button_frame, text="复制到剪贴板", command=lambda: self.copy_to_clipboard(prompt_text))
        copy_btn.pack(pady=5)

    def copy_to_clipboard(self, text_widget):
        """将文本复制到剪贴板"""
        # 启用文本框以便复制
        text_widget.config(state=tk.NORMAL)
        text = text_widget.get(1.0, tk.END)
        self.root.clipboard_clear()
        self.root.clipboard_append(text)
        text_widget.config(state=tk.DISABLED)
        messagebox.showinfo("成功", "已复制到剪贴板")

    def _update_ui_after_response(self, response):
        """更新UI显示LLM响应"""
        # 清空结果显示文本框
        self.result_text.delete(1.0, tk.END)
        # 直接展示结果
        self.result_text.insert(tk.END, response)

        self.status_var.set("就绪")

    def _parse_level_range(self, level_range_text):
        """解析水平范围文本为数值列表，并移除小数点后的零"""
        level_range = []

        # 处理类似 "1.00 - 2.00" 或 "1.00~2.00" 的格式
        range_pattern = r'([\d.]+)\s*[-~]\s*([\d.]+)'
        range_match = re.search(range_pattern, level_range_text)

        if range_match:
            try:
                # 转换为浮点数
                min_val = float(range_match.group(1))
                max_val = float(range_match.group(2))

                # 如果是整数，转换为int以去除小数点，否则保留原浮点数
                if min_val.is_integer():
                    min_val = int(min_val)
                if max_val.is_integer():
                    max_val = int(max_val)

                level_range = [min_val, max_val]
                return level_range
            except ValueError:
                pass

        # 处理类似 "1.00, 2.00, 3.00" 的格式
        list_pattern = r'([\d.]+)(?:,\s*([\d.]+))+'
        list_match = re.search(list_pattern, level_range_text)

        if list_match:
            try:
                values = []
                for val_str in re.findall(r'[\d.]+', level_range_text):
                    val = float(val_str)
                    # 如果是整数，转换为int以去除小数点
                    if val.is_integer():
                        val = int(val)
                    values.append(val)
                level_range = values
                return level_range
            except ValueError:
                pass

        # 处理单个数值的情况
        single_pattern = r'([\d.]+)'
        single_match = re.search(single_pattern, level_range_text)

        if single_match:
            try:
                value = float(single_match.group(1))
                # 如果是整数，转换为int以去除小数点
                if value.is_integer():
                    value = int(value)
                level_range = [value]
                return level_range
            except ValueError:
                pass

        # 如果无法解析为数值，保留原始文本
        return [level_range_text]

    def continue_conversation(self):
        """继续上一次的对话"""
        if not self.conversation_history:
            messagebox.showerror("错误", "没有历史对话，请先处理一个需求。")
            return

        last_answer = self.conversation_history[-1][1]
        self.history += last_answer + "\n"

        continue_input = self.entry.get()
        if not continue_input:
            messagebox.showerror("错误", "请输入继续对话的内容。")
            return

        self.history += continue_input + "\n"

        # 在新线程中调用LLM
        self.status_var.set("正在继续对话...")
        threading.Thread(target=self._continue_conversation_thread, args=(continue_input,), daemon=True).start()

    def _continue_conversation_thread(self, continue_input):
        """在后台线程中继续对话"""
        try:
            # 结合历史对话和自定义prompt
            full_prompt = f"{self.history}\n{self.custom_prompt}"

            # 如果有向量存储，使用RAG
            if self.vectorstore and self.qa_chain:
                # 保存完整的prompt（包括RAG上下文）
                self.last_prompt = full_prompt

                result = self.qa_chain({"query": full_prompt})
                response = result["result"]

                # 添加RAG检索的上下文到prompt
                if "source_documents" in result:
                    context = "\n\n===== RAG检索的上下文 ====="
                    for doc in result["source_documents"]:
                        context += f"\n\n来源: {doc.metadata['source']} (第{doc.metadata.get('page', '未知')}页)\n"
                        context += f"{doc.page_content[:500]}..."  # 限制长度为500个字符

                    self.last_prompt += context
            else:
                # 没有向量存储，直接调用LLM
                self.last_prompt = full_prompt
                response = self.llm(full_prompt)

            # 存储这一轮的提问和回答
            self.conversation_history.append((continue_input, response))

            # 更新UI
            self.root.after(0, self._update_ui_after_response, response)

        except Exception as e:
            self.root.after(0, lambda: messagebox.showerror("错误", f"调用API时发生错误: {str(e)}"))
            self.status_var.set(f"发生错误: {str(e)}")

    def view_history(self):
        """查看历史对话"""
        self.history_text.delete(1.0, tk.END)
        self.history_text.insert(tk.END, self.history)

    def go_back(self):
        """返回上一步对话"""
        if len(self.conversation_history) < 2:
            messagebox.showerror("错误", "没有上一步的对话记录。")
            return

        # 移除当前最新的一轮对话
        self.conversation_history.pop()
        last_question, last_answer = self.conversation_history[-1]

        # 更新历史
        self.history = ""
        for q, a in self.conversation_history[:-1]:
            self.history += q + "\n" + a + "\n"
        self.history += last_question + "\n"

        # 清空结果显示文本框
        self.result_text.delete(1.0, tk.END)
        # 显示上一步的回答
        self.result_text.insert(tk.END, last_answer)

        # 清空输入框并显示上一步的提问
        self.entry.delete(0, tk.END)
        self.entry.insert(0, last_question)

    def show_table_from_result(self):
        """从结果文本中提取表格数据并显示在表格中，自动删除分隔行"""
        result = self.result_text.get(1.0, tk.END)

        # 尝试提取Markdown表格
        table_pattern = r'(\|.*\|[\r\n]+)+\|.*\|'
        match = re.search(table_pattern, result, re.MULTILINE)

        if match:
            # 处理Markdown表格
            table_text = match.group(0)
            lines = table_text.strip().split('\n')

            # 过滤掉包含 "|--- | --- | --- |" 的分隔行和空行
            filtered_lines = []
            for line in lines:
                stripped_line = line.strip()
                # 排除分隔行和空行 - 增强的正则表达式匹配各种分隔行格式
                if stripped_line and not re.match(r'^\|?\s*---+\s*\|+\s*---+\s*\|+\s*---+\s*\|?$', stripped_line):
                    filtered_lines.append(stripped_line)

            # 清空表格
            for i in self.table.get_children():
                self.table.delete(i)

            # 插入数据到表格（跳过表头行）
            # 检查是否有表头行
            has_header = False
            if filtered_lines and len(filtered_lines) > 0:
                header_cells = [part.strip() for part in filtered_lines[0].strip('|').split('|')]
                # 检查表头是否包含预期的列名
                if len(header_cells) >= 3 and header_cells[0] in ['因子', '因素'] and header_cells[1] in ['水平范围',
                                                                                                          '水平']:
                    has_header = True

            # 确定数据行的起始索引
            start_index = 1 if has_header else 0

            for line in filtered_lines[start_index:]:
                if not line.startswith('|'):
                    continue

                parts = [part.strip() for part in line.strip('|').split('|')]
                if len(parts) < 3:
                    continue

                factor = parts[0]
                level_range_text = parts[1]
                unit = parts[2]

                # 解析水平范围（包含去除小数点的逻辑）
                level_range = self._parse_level_range(level_range_text)
                level_range_str = f"{level_range[0]} - {level_range[1]}" if len(level_range) > 1 else str(
                    level_range[0])

                self.table.insert("", "end", values=(factor, level_range_str, unit))
            return

        # 尝试提取HTML表格
        html_table_pattern = r'<table>.*?</table>'
        html_match = re.search(html_table_pattern, result, re.DOTALL)

        if html_match:
            # 提取HTML表格
            html_table = html_match.group(0)
            rows = re.findall(r'<tr>(.*?)</tr>', html_table, re.DOTALL)

            # 清空表格
            for i in self.table.get_children():
                self.table.delete(i)

            if len(rows) > 1:  # 至少有表头和一行数据
                # 处理数据行（跳过表头）
                for row in rows[1:]:
                    cols = re.findall(r'<td>(.*?)</td>', row)
                    if len(cols) >= 3:  # 至少有因子、水平范围和单位
                        factor = cols[0].strip()
                        level_range_text = cols[1].strip()
                        unit = cols[2].strip()

                        # 解析水平范围（包含去除小数点的逻辑）
                        level_range = self._parse_level_range(level_range_text)
                        level_range_str = f"{level_range[0]} - {level_range[1]}" if len(level_range) > 1 else str(
                            level_range[0])

                        self.table.insert("", "end", values=(factor, level_range_str, unit))
            return

        # 尝试提取列表格式
        list_pattern = r'因子\s*:\s*([^\n]+)\s*水平范围\s*:\s*([^\n]+)\s*单位\s*:\s*([^\n]+)'
        list_matches = re.findall(list_pattern, result, re.IGNORECASE)

        if list_matches:
            # 清空表格
            for i in self.table.get_children():
                self.table.delete(i)

            for match in list_matches:
                factor = match[0].strip()
                level_range_text = match[1].strip()
                unit = match[2].strip()

                # 解析水平范围（包含去除小数点的逻辑）
                level_range = self._parse_level_range(level_range_text)
                level_range_str = f"{level_range[0]} - {level_range[1]}" if len(level_range) > 1 else str(
                    level_range[0])

                self.table.insert("", "end", values=(factor, level_range_str, unit))
            return

        # 如果都没有找到
        messagebox.showinfo("提示", "未找到因子和水平的表格内容")

    def add_factor(self):
        """添加新因子到表格"""
        # 创建对话框
        dialog = tk.Toplevel(self.root)
        dialog.title("添加新因子")
        dialog.geometry("400x300")
        dialog.transient(self.root)
        dialog.grab_set()

        # 居中显示
        dialog.update_idletasks()
        width = dialog.winfo_width()
        height = dialog.winfo_height()
        x = (self.root.winfo_width() // 2) - (width // 2) + self.root.winfo_x()
        y = (self.root.winfo_height() // 2) - (height // 2) + self.root.winfo_y()
        dialog.geometry(f"+{x}+{y}")

        # 添加输入框
        frame = tk.Frame(dialog, padx=20, pady=20)
        frame.pack(fill=tk.BOTH, expand=True)

        tk.Label(frame, text="因子名称:").grid(row=0, column=0, sticky=tk.W, pady=5)
        factor_entry = tk.Entry(frame, width=30)
        factor_entry.grid(row=0, column=1, pady=5)

        tk.Label(frame, text="水平范围:").grid(row=1, column=0, sticky=tk.W, pady=5)
        level_entry = tk.Entry(frame, width=30)
        level_entry.grid(row=1, column=1, pady=5)
        tk.Label(frame, text="例如: 1 - 5 或 2, 3, 4").grid(row=2, column=1, sticky=tk.W, pady=2)

        tk.Label(frame, text="单位:").grid(row=3, column=0, sticky=tk.W, pady=5)
        unit_entry = tk.Entry(frame, width=30)
        unit_entry.grid(row=3, column=1, pady=5)

        # 确认按钮
        def confirm():
            factor = factor_entry.get().strip()
            level_range = level_entry.get().strip()
            unit = unit_entry.get().strip()

            if not factor or not level_range:
                messagebox.showerror("错误", "因子名称和水平范围不能为空")
                return

            # 解析水平范围（包含去除小数点的逻辑）
            parsed_levels = self._parse_level_range(level_range)
            level_range_str = f"{parsed_levels[0]} - {parsed_levels[1]}" if len(parsed_levels) > 1 else str(
                parsed_levels[0])

            # 添加到表格
            self.table.insert("", "end", values=(factor, level_range_str, unit))
            dialog.destroy()

        button_frame = tk.Frame(dialog)
        button_frame.pack(pady=10)

        tk.Button(button_frame, text="确认", command=confirm).pack(side=tk.LEFT, padx=10)
        tk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=10)

    def edit_factor(self):
        """编辑选中的因子"""
        selected = self.table.selection()
        if not selected:
            messagebox.showerror("错误", "请先选中要编辑的因子")
            return

        # 获取选中行的数据
        item = selected[0]
        values = self.table.item(item, "values")

        # 创建对话框
        dialog = tk.Toplevel(self.root)
        dialog.title("编辑因子")
        dialog.geometry("400x300")
        dialog.transient(self.root)
        dialog.grab_set()

        # 居中显示
        dialog.update_idletasks()
        width = dialog.winfo_width()
        height = dialog.winfo_height()
        x = (self.root.winfo_width() // 2) - (width // 2) + self.root.winfo_x()
        y = (self.root.winfo_height() // 2) - (height // 2) + self.root.winfo_y()
        dialog.geometry(f"+{x}+{y}")

        # 添加输入框并填充现有数据
        frame = tk.Frame(dialog, padx=20, pady=20)
        frame.pack(fill=tk.BOTH, expand=True)

        tk.Label(frame, text="因子名称:").grid(row=0, column=0, sticky=tk.W, pady=5)
        factor_entry = tk.Entry(frame, width=30)
        factor_entry.grid(row=0, column=1, pady=5)
        factor_entry.insert(0, values[0])

        tk.Label(frame, text="水平范围:").grid(row=1, column=0, sticky=tk.W, pady=5)
        level_entry = tk.Entry(frame, width=30)
        level_entry.grid(row=1, column=1, pady=5)
        level_entry.insert(0, values[1])
        tk.Label(frame, text="例如: 1 - 5 或 2, 3, 4").grid(row=2, column=1, sticky=tk.W, pady=2)

        tk.Label(frame, text="单位:").grid(row=3, column=0, sticky=tk.W, pady=5)
        unit_entry = tk.Entry(frame, width=30)
        unit_entry.grid(row=3, column=1, pady=5)
        unit_entry.insert(0, values[2])

        # 确认按钮
        def confirm():
            factor = factor_entry.get().strip()
            level_range = level_entry.get().strip()
            unit = unit_entry.get().strip()

            if not factor or not level_range:
                messagebox.showerror("错误", "因子名称和水平范围不能为空")
                return

            # 解析水平范围（包含去除小数点的逻辑）
            parsed_levels = self._parse_level_range(level_range)
            level_range_str = f"{parsed_levels[0]} - {parsed_levels[1]}" if len(parsed_levels) > 1 else str(
                parsed_levels[0])

            # 更新表格数据
            self.table.item(item, values=(factor, level_range_str, unit))
            dialog.destroy()

        button_frame = tk.Frame(dialog)
        button_frame.pack(pady=10)

        tk.Button(button_frame, text="确认", command=confirm).pack(side=tk.LEFT, padx=10)
        tk.Button(button_frame, text="取消", command=dialog.destroy).pack(side=tk.LEFT, padx=10)

    def delete_factor(self):
        """删除选中的因子"""
        selected = self.table.selection()
        if not selected:
            messagebox.showerror("错误", "请先选中要删除的因子")
            return

        # 确认删除
        if messagebox.askyesno("确认", "确定要删除选中的因子吗?"):
            for item in selected:
                self.table.delete(item)

    def export_to_csv(self):
        """将表格数据导出为CSV文件"""
        if not self.table.get_children():
            messagebox.showerror("错误", "表格中没有数据可导出")
            return

        export_dir = "./data/csv/"
        os.makedirs(export_dir, exist_ok=True)

        now_str = datetime.now().strftime("%Y-%m-%d_%H%M%S")
        file_path = os.path.join(export_dir, f"{now_str}.csv")

        try:
            with open(file_path, mode='w', newline='', encoding='utf-8') as file:
                writer = csv.writer(file)
                writer.writerow(["因子", "水平范围", "单位"])
                factor_count = 0
                for item in self.table.get_children():
                    factor_count += 1
                    values = self.table.item(item, "values")
                    writer.writerow(values)

            messagebox.showinfo("成功", f"数据已成功导出到:\n{file_path}")

            if hasattr(self, 'on_complete') and callable(self.on_complete):
                self.on_complete(factor_count, file_path)

        except Exception as e:
            messagebox.showerror("错误", f"导出CSV文件时出错:\n{str(e)}")


if __name__ == "__main__":
    # 启用多线程支持
    multiprocessing.set_start_method('spawn')

    root = tk.Tk()
    root.title("实验设计(DOE)辅助工具")
    root.geometry("1200x800")
    app = ExperimentDesignApp(root)
    root.mainloop()
